/*-------------------------------------------------------------------------
 *
 * cluster.c
 *	  根据索引 CLUSTER 表。  这现在也用于 VACUUM FULL。
 *
 * Paul Brown 的原始实现几乎没有剩下...
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994-5, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/commands/cluster.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/amapi.h"
#include "access/heapam.h"
#include "access/multixact.h"
#include "access/relscan.h"
#include "access/tableam.h"
#include "access/toast_internals.h"
#include "access/transam.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
#include "catalog/index.h"
#include "catalog/namespace.h"
#include "catalog/objectaccess.h"
#include "catalog/partition.h"
#include "catalog/pg_am.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
#include "commands/cluster.h"
#include "commands/defrem.h"
#include "commands/progress.h"
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/pg_rusage.h"
#include "utils/relmapper.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
#include "utils/tuplesort.h"

/*
 * 这个结构用于传递有关待聚类表的信息。我们需要这个结构，以便在没有特定表/索引对时调用时可以列出它们。
 */
typedef struct
{
	Oid			tableOid;
	Oid			indexOid;
} RelToCluster;


static void fc_cluster_multiple_rels(List *fc_rtcs, ClusterParams *fc_params);
static void fc_rebuild_relation(Relation fc_OldHeap, Oid fc_indexOid, bool fc_verbose);
static void fc_copy_table_data(Oid fc_OIDNewHeap, Oid fc_OIDOldHeap, Oid fc_OIDOldIndex,
							bool fc_verbose, bool *fc_pSwapToastByContent,
							TransactionId *fc_pFreezeXid, MultiXactId *fc_pCutoffMulti);
static List *fc_get_tables_to_cluster(MemoryContext fc_cluster_context);
static List *fc_get_tables_to_cluster_partitioned(MemoryContext fc_cluster_context,
											   Oid fc_indexOid);


/*---------------------------------------------------------------------------
 * 这个聚类代码允许一次对多个表进行聚类。因为这个原因，我们不能仅在单个事务中运行所有操作，否则我们将被迫同时对所有被聚类的表获取排他性锁，这很可能导致死锁。
 *
 * 为了解决这个问题，我们遵循与VACUUM代码类似的策略，在单独的事务中聚类每个关系。为了使其工作，我们需要：
 *	- 提供一个独立的内存上下文，以便我们可以以一种跨事务存活的方式传递信息
 *	- 每次聚类新关系时启动一个新事务
 *	- 检查待聚类关系的信息的有效性，因为可能有人在我们不知情的情况下删除了一个关系，或者在不同索引上聚类了一个
 *	- 结束事务
 *
 * 单关系情况没有任何这样的开销。
 *
 * 我们还允许在没有索引的情况下指定一个关系。在这种情况下，将查找indisclustered位，如果没有设置该位的索引，将抛出一个ERROR。
 *---------------------------------------------------------------------------
 */
void cluster(ParseState *fc_pstate, ClusterStmt *fc_stmt, bool fc_isTopLevel)
{
	ListCell   *fc_lc;
	ClusterParams fc_params = {0};
	bool		fc_verbose = false;
	Relation	fc_rel = NULL;
	Oid			fc_indexOid = InvalidOid;
	MemoryContext fc_cluster_context;
	List	   *fc_rtcs;

	/* 解析选项列表 */
	foreach(fc_lc, fc_stmt->params)
	{
		DefElem    *fc_opt = (DefElem *) lfirst(fc_lc);

		if (strcmp(fc_opt->defname, "verbose") == 0)
			fc_verbose = defGetBoolean(fc_opt);
		else
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("unrecognized CLUSTER option \"%s\"",
							fc_opt->defname),
					 parser_errposition(fc_pstate, fc_opt->location)));
	}

	fc_params.options = (fc_verbose ? CLUOPT_VERBOSE : 0);

	if (fc_stmt->relation != NULL)
	{
		/* 这是单关系的情况。 */
		Oid			fc_tableOid;

		/*
		 * 查找、锁定并检查表的权限。我们立即获取
		 * AccessExclusiveLock，以避免在单事务情况下的锁升级危害。
		 */
		fc_tableOid = RangeVarGetRelidExtended(fc_stmt->relation,
											AccessExclusiveLock,
											0,
											RangeVarCallbackOwnsTable, NULL);
		fc_rel = table_open(fc_tableOid, NoLock);

		/*
		 * 拒绝对远程临时表进行聚类...它们的本地缓冲区管理器无法应对。
		 */
		if (RELATION_IS_OTHER_TEMP(fc_rel))
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot cluster temporary tables of other sessions")));

		if (fc_stmt->indexname == NULL)
		{
			ListCell   *fc_index;

			/* 我们需要找到设置了indisclustered的索引。 */
			foreach(fc_index, RelationGetIndexList(fc_rel))
			{
				fc_indexOid = lfirst_oid(fc_index);
				if (get_index_isclustered(fc_indexOid))
					break;
				fc_indexOid = InvalidOid;
			}

			if (!OidIsValid(fc_indexOid))
				ereport(ERROR,
						(errcode(ERRCODE_UNDEFINED_OBJECT),
						 errmsg("there is no previously clustered index for table \"%s\"",
								fc_stmt->relation->relname)));
		}
		else
		{
			/*
			 * 预计索引与关系在同一命名空间中。
			 */
			fc_indexOid = get_relname_relid(fc_stmt->indexname,
										 fc_rel->rd_rel->relnamespace);
			if (!OidIsValid(fc_indexOid))
				ereport(ERROR,
						(errcode(ERRCODE_UNDEFINED_OBJECT),
						 errmsg("index \"%s\" for table \"%s\" does not exist",
								fc_stmt->indexname, fc_stmt->relation->relname)));
		}

		if (fc_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
		{
			/* 关闭关系，直到提交保持锁 */
			table_close(fc_rel, NoLock);

			/* 执行操作。 */
			cluster_rel(fc_tableOid, fc_indexOid, &fc_params);

			return;
		}
	}

	/*
	 * 到这里，我们知道我们处于多表情况。为了避免
	 * 持有锁的时间过长，我们希望在各自的
	 * 事务中处理每个表。这迫使我们不允许在用户
	 * 事务块中运行。
	 */
	PreventInTransactionBlock(fc_isTopLevel, "CLUSTER");

	/* 此外，我们需要一个内存上下文来保存我们的关系列表 */
	fc_cluster_context = AllocSetContextCreate(PortalContext,
											"Cluster",
											ALLOCSET_DEFAULT_SIZES);

	/*
	 * 要么我们正在处理一个分区表，要么根本没有给出任何
	 * 表名。在这两种情况下，获取待处理的关系列表。
	 *
	 * 在前者情况下，必须给出索引名称，因此我们不需要重新检查其
	 * "indisclustered"位，但是我们必须检查它是一个我们可以聚类的索引。在后者情况下，我们设置
	 * 选项位以验证indisclustered。
	 *
	 * 在所有情况下，重新检查关系本身是必要的。
	 */
	fc_params.options |= CLUOPT_RECHECK;
	if (fc_rel != NULL)
	{
		Assert(fc_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
		check_index_is_clusterable(fc_rel, fc_indexOid, AccessShareLock);
		fc_rtcs = fc_get_tables_to_cluster_partitioned(fc_cluster_context, fc_indexOid);

		/* 关闭关系，释放对父表的锁 */
		table_close(fc_rel, AccessExclusiveLock);
	}
	else
	{
		fc_rtcs = fc_get_tables_to_cluster(fc_cluster_context);
		fc_params.options |= CLUOPT_RECHECK_ISCLUSTERED;
	}

	/* 执行操作。 */
	fc_cluster_multiple_rels(fc_rtcs, &fc_params);

	/* 开始一个新的事务用于清理工作。 */
	StartTransactionCommand();

	/* 清理工作存储 */
	MemoryContextDelete(fc_cluster_context);
}

/*
 * 给定一组要聚类的关系，分别在不同的事务中处理每一个。
 *
 * 我们期望在开始时处于事务中，但在返回时却没有一个事务。
 */
static void fc_cluster_multiple_rels(List *fc_rtcs, ClusterParams *fc_params)
{
	ListCell   *fc_lc;

	/* 提交以退出启动事务 */
	PopActiveSnapshot();
	CommitTransactionCommand();

	/* 聚类表格，每个在一个单独的事务中 */
	foreach(fc_lc, fc_rtcs)
	{
		RelToCluster *fc_rtc = (RelToCluster *) lfirst(fc_lc);

		/* 为每个关系开始一个新的事务。 */
		StartTransactionCommand();

		/* 索引中的函数可能需要设置快照 */
		PushActiveSnapshot(GetTransactionSnapshot());

		/* 执行操作。 */
		cluster_rel(fc_rtc->tableOid, fc_rtc->indexOid, fc_params);

		PopActiveSnapshot();
		CommitTransactionCommand();
	}
}

/*
 * cluster_rel
 *
 * 这个函数通过创建一个新的聚类表并交换新表和旧表的
 * relfilenodes 来聚类表，从而保留原始表的 OID。这样，我们
 * 不会丢失 GRANT、继承或对于此表的引用（这是 7.3 版本及之前
 * 的一个错误）。
 *
 * 索引也会通过 REINDEX 重建。因为我们实际上是在批量加载
 * 新表，最好在之后创建索引，而不是在加载表的同时逐步填充。
 *
 * 如果 indexOid 为 InvalidOid，表将按物理顺序重写，而不是按
 * 索引顺序。这是 VACUUM FULL 的新实现，错误消息应将操作称为
 * VACUUM 而不是 CLUSTER。
 */
void cluster_rel(Oid fc_tableOid, Oid fc_indexOid, ClusterParams *fc_params)
{
	Relation	fc_OldHeap;
	Oid			fc_save_userid;
	int			fc_save_sec_context;
	int			fc_save_nestlevel;
	bool		fc_verbose = ((fc_params->options & CLUOPT_VERBOSE) != 0);
	bool		fc_recheck = ((fc_params->options & CLUOPT_RECHECK) != 0);

	/* 检查用户请求的中止。 */
	CHECK_FOR_INTERRUPTS();

	pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, fc_tableOid);
	if (OidIsValid(fc_indexOid))
		pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND,
									 PROGRESS_CLUSTER_COMMAND_CLUSTER);
	else
		pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND,
									 PROGRESS_CLUSTER_COMMAND_VACUUM_FULL);

	/*
	 * 我们在事务期间对目标关系和索引获得独占访问。
	 * （对于单一事务的情况，这是多余的，因为 cluster() 已经
	 * 做了这一步。）索引锁在 check_index_is_clusterable 内部
	 * 被获取。
	 */
	fc_OldHeap = try_relation_open(fc_tableOid, AccessExclusiveLock);

	/* 如果表已经不存在，我们可以跳过处理 */
	if (!fc_OldHeap)
	{
		pgstat_progress_end_command();
		return;
	}

	/*
	 * 切换到表所有者的用户ID，以便任何索引函数以该用户身份运行。
	 * 同时锁定安全性受限的操作，并安排将 GUC 变量更改限制在此命令中。
	 */
	GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
	SetUserIdAndSecContext(fc_OldHeap->rd_rel->relowner,
						   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
	fc_save_nestlevel = NewGUCNestLevel();

	/*
	 * 因为我们可能会为每个关系打开一个新事务，所以我们必须检查
	 * 该关系是否仍然是我们想的那样。
	 *
	 * 如果这是一个单一事务的 CLUSTER，我们可以跳过这些测试。
	 * 我们 *必须* 跳过对 indisclustered 的检查，因为它会拒绝
	 * 对未曾聚集的索引进行聚集的尝试。
	 */
	if (fc_recheck)
	{
		/* 检查用户仍然拥有该关系 */
		if (!pg_class_ownercheck(fc_tableOid, fc_save_userid))
		{
			relation_close(fc_OldHeap, AccessExclusiveLock);
			goto out;
		}

		/*
		 * 对于远程会话，静默跳过临时表。仅在 "重新检查" 情况下执行
		 * 此检查是合适的（这目前意味着有人正在执行全数据库的
		 * CLUSTER 或在分区表上），因为在 cluster() 中有另一个检查
		 * 会阻止通过名称聚类远程临时表。在 cluster_rel 中还有另一个
		 * 检查是多余的，但为了额外的安全性我们保留它。
		 */
		if (RELATION_IS_OTHER_TEMP(fc_OldHeap))
		{
			relation_close(fc_OldHeap, AccessExclusiveLock);
			goto out;
		}

		if (OidIsValid(fc_indexOid))
		{
			/*
			 * 检查索引是否仍然存在
			 */
			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(fc_indexOid)))
			{
				relation_close(fc_OldHeap, AccessExclusiveLock);
				goto out;
			}

			/*
			 * 如果需要，检查索引是否仍然是标记为 indisclustered 的索引。
			 */
			if ((fc_params->options & CLUOPT_RECHECK_ISCLUSTERED) != 0 &&
				!get_index_isclustered(fc_indexOid))
			{
				relation_close(fc_OldHeap, AccessExclusiveLock);
				goto out;
			}
		}
	}

	/*
	 * 我们允许 VACUUM FULL，但不允许在共享目录上使用 CLUSTER。CLUSTER
	 * 在大多数方面是有效的，但索引仅会在当前数据库中标记为未聚集，这会导致
	 * 如果后来在另一个数据库中调用 CLUSTER 时出现意外行为。
	 */
	if (OidIsValid(fc_indexOid) && fc_OldHeap->rd_rel->relisshared)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot cluster a shared catalog")));

	/*
	 * 不处理其他后端的临时表……它们的本地缓冲区管理器无法应对。
	 */
	if (RELATION_IS_OTHER_TEMP(fc_OldHeap))
	{
		if (OidIsValid(fc_indexOid))
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot cluster temporary tables of other sessions")));
		else
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("cannot vacuum temporary tables of other sessions")));
	}

	/*
	 * 还要检查当前事务中对关系的活跃使用，
	 * 包括开放扫描和待处理的 AFTER 触发器事件。
	 */
	CheckTableNotInUse(fc_OldHeap, OidIsValid(fc_indexOid) ? "CLUSTER" : "VACUUM");

	/* 检查堆和索引是否有效以进行聚集 */
	if (OidIsValid(fc_indexOid))
		check_index_is_clusterable(fc_OldHeap, fc_indexOid, AccessExclusiveLock);

	/*
	 * 如果这是一个尚未从查询中填充的物化视图，则静默忽略请求。
	 * 不会造成任何损害，因为没有数据可处理，
	 * 我们不想抛出错误，如果这是多关系请求的一部分——
	 * 例如，CLUSTER 在整个数据库上运行。
	 */
	if (fc_OldHeap->rd_rel->relkind == RELKIND_MATVIEW &&
		!RelationIsPopulated(fc_OldHeap))
	{
		relation_close(fc_OldHeap, AccessExclusiveLock);
		goto out;
	}

	Assert(fc_OldHeap->rd_rel->relkind == RELKIND_RELATION ||
		   fc_OldHeap->rd_rel->relkind == RELKIND_MATVIEW ||
		   fc_OldHeap->rd_rel->relkind == RELKIND_TOASTVALUE);

	/*
	 * 所有对元组或页面的谓词锁即将失效，因为我们会移动元组。
	 * 将它们提升为关系锁。对索引的谓词锁将在重新索引时提升。
	 */
	TransferPredicateLocksToHeapRelation(fc_OldHeap);

	/* rebuild_relation 执行所有脏活 */
	fc_rebuild_relation(fc_OldHeap, fc_indexOid, fc_verbose);

	/* 注意：rebuild_relation 在 OldHeap 上执行 table_close() */

out:
	/* 回滚索引函数执行的任何 GUC 更改 */
	AtEOXact_GUC(false, fc_save_nestlevel);

	/* 恢复 userid 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	pgstat_progress_end_command();
}

/*
 * 验证指定的堆和索引是否有效以进行聚集
 *
 * 副作用：在索引上获得锁。调用者在某些情况下可能已经
 * 对表拥有 AccessExclusiveLock，但并不是所有情况下，
 * 所以我们不能依赖表级锁在此处进行保护。
 */
void check_index_is_clusterable(Relation fc_OldHeap, Oid fc_indexOid, LOCKMODE fc_lockmode)
{
	Relation	fc_OldIndex;

	fc_OldIndex = index_open(fc_indexOid, fc_lockmode);

	/*
	 * 检查索引实际上是否是给定关系上的索引
	 */
	if (fc_OldIndex->rd_index == NULL ||
		fc_OldIndex->rd_index->indrelid != RelationGetRelid(fc_OldHeap))
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("\"%s\" is not an index for table \"%s\"",
						RelationGetRelationName(fc_OldIndex),
						RelationGetRelationName(fc_OldHeap))));

	/* 索引 AM 必须允许聚集 */
	if (!fc_OldIndex->rd_indam->amclusterable)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot cluster on index \"%s\" because access method does not support clustering",
						RelationGetRelationName(fc_OldIndex))));

	/*
	 * 不允许对不完整的索引（可能未对关系的每一行进行索引的索引）进行聚集。
	 * 我们可以通过对表进行单独的 seqscan 处理来复制缺失的行，但这似乎
	 * 成本高昂且繁琐。
	 */
	if (!heap_attisnull(fc_OldIndex->rd_indextuple, Anum_pg_index_indpred, NULL))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot cluster on partial index \"%s\"",
						RelationGetRelationName(fc_OldIndex))));

	/*
	 * 如果索引是来自失败的 CREATE INDEX CONCURRENTLY 的残留，则不允许；
	 * 它可能并不包含每个堆行的条目，或者可能甚至不一致。
	 * （但请注意，我们不检查 indcheckxmin；
	 * 遵循损坏的 HOT 链的最坏后果是我们可能会在新表中以错误顺序放置
	 * 最近死掉的元组，这毫无害处。）
	 */
	if (!fc_OldIndex->rd_index->indisvalid)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot cluster on invalid index \"%s\"",
						RelationGetRelationName(fc_OldIndex))));

	/* 在 OldIndex 上删除 relcache 引用计数，但保持锁定 */
	index_close(fc_OldIndex, NoLock);
}

/*
 * mark_index_clustered：将指定索引标记为聚集索引
 *
 * 当 indexOid == InvalidOid 时，将标记所有未聚集的关系索引。
 */
void mark_index_clustered(Relation fc_rel, Oid fc_indexOid, bool fc_is_internal)
{
	HeapTuple	fc_indexTuple;
	Form_pg_index fc_indexForm;
	Relation	fc_pg_index;
	ListCell   *fc_index;

	/* 不允许应用于分区表 */
	if (fc_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot mark index clustered in partitioned table")));

	/*
	 * 如果索引已标记为聚集，则无需执行任何操作。
	 */
	if (OidIsValid(fc_indexOid))
	{
		if (get_index_isclustered(fc_indexOid))
			return;
	}

	/*
	 * 检查关系的每个索引，并根据需要设置/清除位。
	 */
	fc_pg_index = table_open(IndexRelationId, RowExclusiveLock);

	foreach(fc_index, RelationGetIndexList(fc_rel))
	{
		Oid			fc_thisIndexOid = lfirst_oid(fc_index);

		fc_indexTuple = SearchSysCacheCopy1(INDEXRELID,
										 ObjectIdGetDatum(fc_thisIndexOid));
		if (!HeapTupleIsValid(fc_indexTuple))
			elog(ERROR, "cache lookup failed for index %u", fc_thisIndexOid);
		fc_indexForm = (Form_pg_index) GETSTRUCT(fc_indexTuple);

		/*
		 * 如果设置，则取消设置该位。我们知道这是错误的，因为我们早些时候
		 * 进行了检查。
		 */
		if (fc_indexForm->indisclustered)
		{
			fc_indexForm->indisclustered = false;
			CatalogTupleUpdate(fc_pg_index, &fc_indexTuple->t_self, fc_indexTuple);
		}
		else if (fc_thisIndexOid == fc_indexOid)
		{
			/* 这在之前已经检查过，但让我们再确认一下 */
			if (!fc_indexForm->indisvalid)
				elog(ERROR, "cannot cluster on invalid index %u", fc_indexOid);
			fc_indexForm->indisclustered = true;
			CatalogTupleUpdate(fc_pg_index, &fc_indexTuple->t_self, fc_indexTuple);
		}

		InvokeObjectPostAlterHookArg(IndexRelationId, fc_thisIndexOid, 0,
									 InvalidOid, fc_is_internal);

		heap_freetuple(fc_indexTuple);
	}

	table_close(fc_pg_index, RowExclusiveLock);
}

/*
 * rebuild_relation: 重建现有关系的索引或物理顺序
 *
 * OldHeap: 待重建的表 --- 必须打开并且独占锁定！
 * indexOid: 用于聚集的索引，或 InvalidOid 以物理顺序重写。
 *
 * 注意：此例程在适当的时候关闭 OldHeap；调用者不应关闭。
 */
static void fc_rebuild_relation(Relation fc_OldHeap, Oid fc_indexOid, bool fc_verbose)
{
	Oid			fc_tableOid = RelationGetRelid(fc_OldHeap);
	Oid			fc_accessMethod = fc_OldHeap->rd_rel->relam;
	Oid			fc_tableSpace = fc_OldHeap->rd_rel->reltablespace;
	Oid			fc_OIDNewHeap;
	char		fc_relpersistence;
	bool		fc_is_system_catalog;
	bool		fc_swap_toast_by_content;
	TransactionId fc_frozenXid;
	MultiXactId fc_cutoffMulti;

	if (OidIsValid(fc_indexOid))
		/* 标记正确的索引为聚集 */
		mark_index_clustered(fc_OldHeap, fc_indexOid, true);

	/* 在关闭 OldHeap 之前记住关于 rel 的信息 */
	fc_relpersistence = fc_OldHeap->rd_rel->relpersistence;
	fc_is_system_catalog = IsSystemRelation(fc_OldHeap);

	/* 关闭 relcache 条目，但在事务提交之前保持锁定 */
	table_close(fc_OldHeap, NoLock);

	/* 创建将接收重新排序数据的临时表 */
	fc_OIDNewHeap = make_new_heap(fc_tableOid, fc_tableSpace,
							   fc_accessMethod,
							   fc_relpersistence,
							   AccessExclusiveLock);

	/* 按所需顺序将堆数据复制到新表中 */
	fc_copy_table_data(fc_OIDNewHeap, fc_tableOid, fc_indexOid, fc_verbose,
					&fc_swap_toast_by_content, &fc_frozenXid, &fc_cutoffMulti);

	/*
	 * 交换目标表和临时表的物理文件，然后
	 * 重建目标的索引并丢弃临时表。
	 */
	finish_heap_swap(fc_tableOid, fc_OIDNewHeap, fc_is_system_catalog,
					 fc_swap_toast_by_content, false, true,
					 fc_frozenXid, fc_cutoffMulti,
					 fc_relpersistence);
}


/*
 * 创建临时表，该表将在 CLUSTER、ALTER TABLE 和类似操作
 * 期间填充新数据。此临时表复制 OldHeap 的逻辑结构；但将具有
 * 指定的物理存储属性 NewTableSpace、NewAccessMethod 和
 * relpersistence。
 *
 * 在此之后，调用者应以转移/修改后的数据加载新堆，然后调用
 * finish_heap_swap 以完成操作。
 */
Oid make_new_heap(Oid fc_OIDOldHeap, Oid fc_NewTableSpace, Oid fc_NewAccessMethod,
			  char fc_relpersistence, LOCKMODE fc_lockmode)
{
	TupleDesc	fc_OldHeapDesc;
	char		fc_NewHeapName[NAMEDATALEN];
	Oid			fc_OIDNewHeap;
	Oid			fc_toastid;
	Relation	fc_OldHeap;
	HeapTuple	fc_tuple;
	Datum		fc_reloptions;
	bool		fc_isNull;
	Oid			fc_namespaceid;

	fc_OldHeap = table_open(fc_OIDOldHeap, fc_lockmode);
	fc_OldHeapDesc = RelationGetDescr(fc_OldHeap);

	/*
	 * 请注意，新堆不会接收与 OldHeap 关联的任何默认值或
	 * 约束；我们不需要它们，也没有理由花费周期将它们插入到
	 * 目录中再删除它们。
	 */

	/*
	 * 但我们确实希望将旧堆的 reloptions 用于新堆。
	 */
	fc_tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(fc_OIDOldHeap));
	if (!HeapTupleIsValid(fc_tuple))
		elog(ERROR, "cache lookup failed for relation %u", fc_OIDOldHeap);
	fc_reloptions = SysCacheGetAttr(RELOID, fc_tuple, Anum_pg_class_reloptions,
								 &fc_isNull);
	if (fc_isNull)
		fc_reloptions = (Datum) 0;

	if (fc_relpersistence == RELPERSISTENCE_TEMP)
		fc_namespaceid = LookupCreationNamespace("pg_temp");
	else
		fc_namespaceid = RelationGetNamespace(fc_OldHeap);

	/*
	 * 创建新堆，使用与现有表相同命名空间中的临时名称。
	 * 注意：存在与用户 relnames 冲突的风险。规避此问题似乎
	 * 比值得麻烦得多；特别是，我们不能在与旧表不同的命名空间中
	 * 创建新堆，否则我们将遇到临时表的 TEMP 状态问题。
	 *
	 * 注意：即使我们正在重建共享关系，新堆也不是共享关系。
	 * 但是，如果源是映射的，我们确实会使新堆映射。这简化了
	 * swap_relation_files，并且对于重建 pg_class 是绝对必要的，
	 * 原因将在那里解释。
	 */
	snprintf(fc_NewHeapName, sizeof(fc_NewHeapName), "pg_temp_%u", fc_OIDOldHeap);

	fc_OIDNewHeap = heap_create_with_catalog(fc_NewHeapName,
										  fc_namespaceid,
										  fc_NewTableSpace,
										  InvalidOid,
										  InvalidOid,
										  InvalidOid,
										  fc_OldHeap->rd_rel->relowner,
										  fc_NewAccessMethod,
										  fc_OldHeapDesc,
										  NIL,
										  RELKIND_RELATION,
										  fc_relpersistence,
										  false,
										  RelationIsMapped(fc_OldHeap),
										  ONCOMMIT_NOOP,
										  fc_reloptions,
										  false,
										  true,
										  true,
										  fc_OIDOldHeap,
										  NULL);
	Assert(fc_OIDNewHeap != InvalidOid);

	ReleaseSysCache(fc_tuple);

	/*
	 * 提前命令计数器，以便新创建的关系的目录元组对
	 * table_open 可见。
	 */
	CommandCounterIncrement();

	/*
	 * 如有必要，为新关系创建 TOAST 表。
	 *
	 * 如果关系还没有 TOAST 表，我们就不需要新关系的 TOAST 表。
	 * 不过，反过来是可能的：如果一些宽列已被删除，NewHeapCreateToastTable
	 * 可以决定新表不需要 TOAST 表。
	 *
	 * 注意，NewHeapCreateToastTable 以 CommandCounterIncrement 结束，
	 * 以便 TOAST 表对插入可见。
	 */
	fc_toastid = fc_OldHeap->rd_rel->reltoastrelid;
	if (OidIsValid(fc_toastid))
	{
		/* 保持现有 TOAST 表的 reloptions（如果有） */
		fc_tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(fc_toastid));
		if (!HeapTupleIsValid(fc_tuple))
			elog(ERROR, "cache lookup failed for relation %u", fc_toastid);
		fc_reloptions = SysCacheGetAttr(RELOID, fc_tuple, Anum_pg_class_reloptions,
									 &fc_isNull);
		if (fc_isNull)
			fc_reloptions = (Datum) 0;

		NewHeapCreateToastTable(fc_OIDNewHeap, fc_reloptions, fc_lockmode, fc_toastid);

		ReleaseSysCache(fc_tuple);
	}

	table_close(fc_OldHeap, NoLock);

	return fc_OIDNewHeap;
}

/*
 * 进行表数据的物理复制。
 *
 * 有三个输出参数：
 * *pSwapToastByContent 如果必须按内容交换 toast 表，则设置为 true。
 * *pFreezeXid 接收用作冻结截断点的 TransactionId。
 * *pCutoffMulti 接收用作截断点的 MultiXactId。
 */
static void fc_copy_table_data(Oid fc_OIDNewHeap, Oid fc_OIDOldHeap, Oid fc_OIDOldIndex, bool fc_verbose,
				bool *fc_pSwapToastByContent, TransactionId *fc_pFreezeXid,
				MultiXactId *fc_pCutoffMulti)
{
	Relation	fc_NewHeap,
				fc_OldHeap,
				fc_OldIndex;
	Relation	fc_relRelation;
	HeapTuple	fc_reltup;
	Form_pg_class fc_relform;
	TupleDesc	fc_oldTupDesc PG_USED_FOR_ASSERTS_ONLY;
	TupleDesc	fc_newTupDesc PG_USED_FOR_ASSERTS_ONLY;
	TransactionId fc_OldestXmin,
				fc_FreezeXid;
	MultiXactId fc_OldestMxact,
				fc_MultiXactCutoff;
	bool		fc_use_sort;
	double		fc_num_tuples = 0,
				fc_tups_vacuumed = 0,
				fc_tups_recently_dead = 0;
	BlockNumber fc_num_pages;
	int			fc_elevel = fc_verbose ? INFO : DEBUG2;
	PGRUsage	fc_ru0;
	char	   *fc_nspname;

	pg_rusage_init(&fc_ru0);

	/*
	 * 打开我们需要的关系。
	 */
	fc_NewHeap = table_open(fc_OIDNewHeap, AccessExclusiveLock);
	fc_OldHeap = table_open(fc_OIDOldHeap, AccessExclusiveLock);
	if (OidIsValid(fc_OIDOldIndex))
		fc_OldIndex = index_open(fc_OIDOldIndex, AccessExclusiveLock);
	else
		fc_OldIndex = NULL;

	/* 为日志记录目的存储命名空间名称的副本 */
	fc_nspname = get_namespace_name(RelationGetNamespace(fc_OldHeap));

	/*
	 * 他们的元组描述符应该完全相同，但在这里我们只需要
	 * 假设它们有相同数量的列。
	 */
	fc_oldTupDesc = RelationGetDescr(fc_OldHeap);
	fc_newTupDesc = RelationGetDescr(fc_NewHeap);
	Assert(fc_newTupDesc->natts == fc_oldTupDesc->natts);

	/*
	 * 如果 OldHeap 有一个 toast 表，获取对 toast 表的锁，以防止
	 * 它被清理。这是因为自动清理进程
	 * 独立于他们的主表处理 toast 表，没有对
	 * 后者的锁。如果在我们
	 * 在下面计算 OldestXmin 后，自动清理开始在 toast 表上，
	 * 它将使用较晚的 OldestXmin，然后
	 * 可能会将我们认为仅为 RECENTLY_DEAD 的主元组
	 * 所属于的 DEAD toast 元组删除。然后我们在尝试复制这些
	 * 元组时将失败。
	 *
	 * 我们不需要在这里打开 toast 关系，只需锁定它。该锁
	 * 将保持到事务结束。
	 */
	if (fc_OldHeap->rd_rel->reltoastrelid)
		LockRelationOid(fc_OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);

	/*
	 * 如果两个表都有 TOAST 表，则按内容交换 toast。旧表可能
	 * 有一个 toast 表，但新表没有，如果可 toast 列已被删除。
	 * 在这种情况下，我们必须按链接进行交换。这是可以的，因为按内容
	 * 交换对系统目录来说是唯一重要的，而我们不支持它们的模式更改。
	 */
	if (fc_OldHeap->rd_rel->reltoastrelid && fc_NewHeap->rd_rel->reltoastrelid)
	{
		*fc_pSwapToastByContent = true;

		/*
		 * 在按内容交换时，写入 NewHeap 的任何 toast 指针
		 * 必须使用旧 toast 表的 OID，因为这就是将来找
		 * 到 toast 数据的地方。通过设置 rd_toastoid 来设置这
		 * 一点。通过这也告诉 toast_save_datum() 保留 toast 值
		 * OIDs，这样我们希望以免使系统目录缓存中的 toast
		 * 指针失效，并避免对单个 toast 值进行多次复制。
		 *
		 * 注意，我们必须保持 NewHeap 打开，直到我们完成写入数据，
		 * 因为 relcache 不会保证在关系关闭后记住这项设置。
		 * 此外，这种技术依赖于没有人会尝试从 NewHeap 中读取，
		 * 直到我们完成写入并交换关系 --- 否则他们可能
		 * 会跟随 toast 指针到错误的位置。（实际上对于从
		 * 旧 toast 表复制过来的值是可以工作的，但对于
		 * 任何我们 toast 的先前未被 toast 的值则不可行。）
		 */
		fc_NewHeap->rd_toastoid = fc_OldHeap->rd_rel->reltoastrelid;
	}
	else
		*fc_pSwapToastByContent = false;

	/*
	 * 计算用于冻结和剔除死元组及多事务的 xids。
	 * 由于我们无论如何都会重写整个表，因此没有理由
	 * 不要对此采取积极措施。
	 */
	vacuum_set_xid_limits(fc_OldHeap, 0, 0, 0, 0, &fc_OldestXmin, &fc_OldestMxact,
						  &fc_FreezeXid, &fc_MultiXactCutoff);

	/*
	 * FreezeXid 将成为表的新 relfrozenxid，且必须不能向
	 * 后退，因此取最大值。
	 */
	if (TransactionIdIsValid(fc_OldHeap->rd_rel->relfrozenxid) &&
		TransactionIdPrecedes(fc_FreezeXid, fc_OldHeap->rd_rel->relfrozenxid))
		fc_FreezeXid = fc_OldHeap->rd_rel->relfrozenxid;

	/*
	 * MultiXactCutoff 同样不应该后退。
	 */
	if (MultiXactIdIsValid(fc_OldHeap->rd_rel->relminmxid) &&
		MultiXactIdPrecedes(fc_MultiXactCutoff, fc_OldHeap->rd_rel->relminmxid))
		fc_MultiXactCutoff = fc_OldHeap->rd_rel->relminmxid;

	/*
	 * 决定是使用 indexscan 还是 seqscan-and-optional-sort 来扫描
	 * OldHeap。我们知道如何使用排序来复制
	 * btree 索引的顺序，如果规划者告诉我们这样做更便宜，
	 * 我们就会使用 seqscan-and-sort。否则，如果提供了索引，
	 * 总是使用 indexscan，否则使用普通的 seqscan。
	 */
	if (fc_OldIndex != NULL && fc_OldIndex->rd_rel->relam == BTREE_AM_OID)
		fc_use_sort = plan_cluster_use_sort(fc_OIDOldHeap, fc_OIDOldIndex);
	else
		fc_use_sort = false;

	/* 记录我们正在做的工作 */
	if (fc_OldIndex != NULL && !fc_use_sort)
		ereport(fc_elevel,
				(errmsg("clustering \"%s.%s\" using index scan on \"%s\"",
						fc_nspname,
						RelationGetRelationName(fc_OldHeap),
						RelationGetRelationName(fc_OldIndex))));
	else if (fc_use_sort)
		ereport(fc_elevel,
				(errmsg("clustering \"%s.%s\" using sequential scan and sort",
						fc_nspname,
						RelationGetRelationName(fc_OldHeap))));
	else
		ereport(fc_elevel,
				(errmsg("vacuuming \"%s.%s\"",
						fc_nspname,
						RelationGetRelationName(fc_OldHeap))));

	/*
	 * 将实际的复制工作交给 AM 特定的函数，通用代码
	 * 不能知道如何处理 AM 之间的可见性。注意，此
	 * 例程被允许将 FreezeXid / MultiXactCutoff 设置为不同的
	 * 值（例如，因为 AM 不使用冻结）。
	 */
	table_relation_copy_for_cluster(fc_OldHeap, fc_NewHeap, fc_OldIndex, fc_use_sort,
									fc_OldestXmin, &fc_FreezeXid, &fc_MultiXactCutoff,
									&fc_num_tuples, &fc_tups_vacuumed,
									&fc_tups_recently_dead);

	/* 返回已选择的值给调用者，设置为 relfrozenxid/minmxid */
	*fc_pFreezeXid = fc_FreezeXid;
	*fc_pCutoffMulti = fc_MultiXactCutoff;

	/* 重置 rd_toastoid 只是为了整洁——它不应该再被查看 */
	fc_NewHeap->rd_toastoid = InvalidOid;

	fc_num_pages = RelationGetNumberOfBlocks(fc_NewHeap);

	/* 记录我们做过的事情 */
	ereport(fc_elevel,
			(errmsg("\"%s.%s\": found %.0f removable, %.0f nonremovable row versions in %u pages",
					fc_nspname,
					RelationGetRelationName(fc_OldHeap),
					fc_tups_vacuumed, fc_num_tuples,
					RelationGetNumberOfBlocks(fc_OldHeap)),
			 errdetail("%.0f dead row versions cannot be removed yet.\n"
					   "%s.",
					   fc_tups_recently_dead,
					   pg_rusage_show(&fc_ru0))));

	if (fc_OldIndex != NULL)
		index_close(fc_OldIndex, NoLock);
	table_close(fc_OldHeap, NoLock);
	table_close(fc_NewHeap, NoLock);

	/* 更新 pg_class 以反映页面和元组的正确值。 */
	fc_relRelation = table_open(RelationRelationId, RowExclusiveLock);

	fc_reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(fc_OIDNewHeap));
	if (!HeapTupleIsValid(fc_reltup))
		elog(ERROR, "cache lookup failed for relation %u", fc_OIDNewHeap);
	fc_relform = (Form_pg_class) GETSTRUCT(fc_reltup);

	fc_relform->relpages = fc_num_pages;
	fc_relform->reltuples = fc_num_tuples;

	/* 不更新 pg_class 的统计信息。参见 swap_relation_files。 */
	if (fc_OIDOldHeap != RelationRelationId)
		CatalogTupleUpdate(fc_relRelation, &fc_reltup->t_self, fc_reltup);
	else
		CacheInvalidateRelcacheByTuple(fc_reltup);

	/* 清理。 */
	heap_freetuple(fc_reltup);
	table_close(fc_relRelation, RowExclusiveLock);

	/* 使更新可见 */
	CommandCounterIncrement();
}

/*
 * 交换两个给定关系的物理文件。
 *
 * 我们交换物理身份 (reltablespace, relfilenode)，同时保持
 * 两个关系的相同逻辑身份。 relpersistence 也被
 * 交换，这一点至关重要，因为它决定了每个
 * 关系的缓冲区位置。
 *
 * 我们可以以两种方式交换关联的 TOAST 数据：递归地交换
 * toast 表（及其索引）的物理内容，或者在给定关系的
 * pg_class 条目中交换 TOAST 链接。前者需要
 * 管理共享目录的重写（在此我们无法更改 pg_class
 * 链接），而后者是处理完全添加或移除 toast
 * 表情况下的唯一方法。
 *
 * 此外，第一个关系的 relfrozenxid 被设置为
 * frozenXid。虽然将其放在这里有点丑，但调用者
 * 无论如何都必须这样做，所以将其放在这里可以节省一次
 * heap_update。注意：在 swap-toast-links 的情况下，
 * 我们假设不需要更改 toast 表的 relfrozenxid：新版本的
 * toast 表应该已经将 relfrozenxid 设置为 RecentXmin，
 * 这已经足够了。
 *
 * 最后，如果 r2 及其 toast 表和 toast 索引（如果有）被映射，
 * 它们的 OID 被发射到 mapped_tables[] 中。这虽然有点
 * 繁琐，但比后面在 finish_heap_swap 中再次查找信息要好。
 */
static void fc_swap_relation_files(Oid fc_r1, Oid fc_r2, bool fc_target_is_pg_class,
					bool fc_swap_toast_by_content,
					bool fc_is_internal,
					TransactionId fc_frozenXid,
					MultiXactId fc_cutoffMulti,
					Oid *fc_mapped_tables)
{
	Relation	fc_relRelation;
	HeapTuple	fc_reltup1,
				fc_reltup2;
	Form_pg_class fc_relform1,
				fc_relform2;
	Oid			fc_relfilenode1,
				fc_relfilenode2;
	Oid			fc_swaptemp;
	char		fc_swptmpchr;
	Oid			fc_relam1,
				fc_relam2;

	/* 我们需要可写的 pg_class 元组副本。 */
	fc_relRelation = table_open(RelationRelationId, RowExclusiveLock);

	fc_reltup1 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(fc_r1));
	if (!HeapTupleIsValid(fc_reltup1))
		elog(ERROR, "cache lookup failed for relation %u", fc_r1);
	fc_relform1 = (Form_pg_class) GETSTRUCT(fc_reltup1);

	fc_reltup2 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(fc_r2));
	if (!HeapTupleIsValid(fc_reltup2))
		elog(ERROR, "cache lookup failed for relation %u", fc_r2);
	fc_relform2 = (Form_pg_class) GETSTRUCT(fc_reltup2);

	fc_relfilenode1 = fc_relform1->relfilenode;
	fc_relfilenode2 = fc_relform2->relfilenode;
	fc_relam1 = fc_relform1->relam;
	fc_relam2 = fc_relform2->relam;

	if (OidIsValid(fc_relfilenode1) && OidIsValid(fc_relfilenode2))
	{
		/*
		 * 正常的非映射关系：交换 relfilenodes、reltablespaces、
		 * relpersistence
		 */
		Assert(!fc_target_is_pg_class);

		fc_swaptemp = fc_relform1->relfilenode;
		fc_relform1->relfilenode = fc_relform2->relfilenode;
		fc_relform2->relfilenode = fc_swaptemp;

		fc_swaptemp = fc_relform1->reltablespace;
		fc_relform1->reltablespace = fc_relform2->reltablespace;
		fc_relform2->reltablespace = fc_swaptemp;

		fc_swaptemp = fc_relform1->relam;
		fc_relform1->relam = fc_relform2->relam;
		fc_relform2->relam = fc_swaptemp;

		fc_swptmpchr = fc_relform1->relpersistence;
		fc_relform1->relpersistence = fc_relform2->relpersistence;
		fc_relform2->relpersistence = fc_swptmpchr;

		/* 如果我们通过链接交换，也交换 toast 链接 */
		if (!fc_swap_toast_by_content)
		{
			fc_swaptemp = fc_relform1->reltoastrelid;
			fc_relform1->reltoastrelid = fc_relform2->reltoastrelid;
			fc_relform2->reltoastrelid = fc_swaptemp;
		}
	}
	else
	{
		/*
		 * 映射关系的情况。这里我们必须交换关系映射
		 * 而不是修改 pg_class 列。两个必须被映射。
		 */
		if (OidIsValid(fc_relfilenode1) || OidIsValid(fc_relfilenode2))
			elog(ERROR, "cannot swap mapped relation \"%s\" with non-mapped relation",
				 NameStr(fc_relform1->relname));

		/*
		 * 我们无法更改映射关系的表空间或持久性，也无法处理其中的 toast 链接交换，因为我们必须不对其 pg_class 行应用任何关键更改。这些情况应通过上游权限测试予以防止，因此这些检查是非用户可见的紧急后备措施。
		 */
		if (fc_relform1->reltablespace != fc_relform2->reltablespace)
			elog(ERROR, "cannot change tablespace of mapped relation \"%s\"",
				 NameStr(fc_relform1->relname));
		if (fc_relform1->relpersistence != fc_relform2->relpersistence)
			elog(ERROR, "cannot change persistence of mapped relation \"%s\"",
				 NameStr(fc_relform1->relname));
		if (fc_relform1->relam != fc_relform2->relam)
			elog(ERROR, "cannot change access method of mapped relation \"%s\"",
				 NameStr(fc_relform1->relname));
		if (!fc_swap_toast_by_content &&
			(fc_relform1->reltoastrelid || fc_relform2->reltoastrelid))
			elog(ERROR, "cannot swap toast by links for mapped relation \"%s\"",
				 NameStr(fc_relform1->relname));

		/*
		 * 获取映射 --- 不应失败，但要谨慎
		 */
		fc_relfilenode1 = RelationMapOidToFilenode(fc_r1, fc_relform1->relisshared);
		if (!OidIsValid(fc_relfilenode1))
			elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u",
				 NameStr(fc_relform1->relname), fc_r1);
		fc_relfilenode2 = RelationMapOidToFilenode(fc_r2, fc_relform2->relisshared);
		if (!OidIsValid(fc_relfilenode2))
			elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u",
				 NameStr(fc_relform2->relname), fc_r2);

		/*
		 * 将替换映射发送到 relmapper。注意，这些映射在 CommandCounterIncrement 之前实际上不会生效。
		 */
		RelationMapUpdateMap(fc_r1, fc_relfilenode2, fc_relform1->relisshared, false);
		RelationMapUpdateMap(fc_r2, fc_relfilenode1, fc_relform2->relisshared, false);

		/* 将映射 r2 表的 OID 传回调用者 */
		*fc_mapped_tables++ = fc_r2;
	}

	/*
	 * 识别 rel1 的 relfilenode（从 rel2 交换）在这个子事务中是新的。 rel2 存储（从 rel1 交换）可能是新的，也可能不是。
	 */
	{
		Relation	fc_rel1,
					fc_rel2;

		fc_rel1 = relation_open(fc_r1, NoLock);
		fc_rel2 = relation_open(fc_r2, NoLock);
		fc_rel2->rd_createSubid = fc_rel1->rd_createSubid;
		fc_rel2->rd_newRelfilenodeSubid = fc_rel1->rd_newRelfilenodeSubid;
		fc_rel2->rd_firstRelfilenodeSubid = fc_rel1->rd_firstRelfilenodeSubid;
		RelationAssumeNewRelfilenode(fc_rel1);
		relation_close(fc_rel1, NoLock);
		relation_close(fc_rel2, NoLock);
	}

	/*
	 * 在共享目录的情况下，接下来的几步将只影响我们自己数据库的 pg_class 行；但是这没关系，因为它们都是非关键更新。这对于映射目录的情况也是一个重要事实，因为我们可能会提交映射更改，然后未能提交 pg_class 更新。
	 */

	/* 设置 rel1 的冻结 Xid 和最小 MultiXid */
	if (fc_relform1->relkind != RELKIND_INDEX)
	{
		Assert(!TransactionIdIsValid(fc_frozenXid) ||
			   TransactionIdIsNormal(fc_frozenXid));
		fc_relform1->relfrozenxid = fc_frozenXid;
		fc_relform1->relminmxid = fc_cutoffMulti;
	}

	/* 也交换大小统计，因为新关系具有新更新的统计信息 */
	{
		int32		fc_swap_pages;
		float4		fc_swap_tuples;
		int32		fc_swap_allvisible;

		fc_swap_pages = fc_relform1->relpages;
		fc_relform1->relpages = fc_relform2->relpages;
		fc_relform2->relpages = fc_swap_pages;

		fc_swap_tuples = fc_relform1->reltuples;
		fc_relform1->reltuples = fc_relform2->reltuples;
		fc_relform2->reltuples = fc_swap_tuples;

		fc_swap_allvisible = fc_relform1->relallvisible;
		fc_relform1->relallvisible = fc_relform2->relallvisible;
		fc_relform2->relallvisible = fc_swap_allvisible;
	}

	/*
	 * 更新 pg_class 中的元组 --- 除非交换的目标关系是 pg_class 本身。在这种情况下，进行更改没有任何意义，因为我们将在即将丢弃的数据上进行更新。由于这里对映射关系所做的实际工作只是更改关系映射设置，因此在这种情况下不更新 pg_class 行是可以的。最重要的更改将随后在 finish_heap_swap() 本身中进行。
	 */
	if (!fc_target_is_pg_class)
	{
		CatalogIndexState fc_indstate;

		fc_indstate = CatalogOpenIndexes(fc_relRelation);
		CatalogTupleUpdateWithInfo(fc_relRelation, &fc_reltup1->t_self, fc_reltup1,
								   fc_indstate);
		CatalogTupleUpdateWithInfo(fc_relRelation, &fc_reltup2->t_self, fc_reltup2,
								   fc_indstate);
		CatalogCloseIndexes(fc_indstate);
	}
	else
	{
		/* 不更新 ... 但我们仍然需要 relcache 无效 */
		CacheInvalidateRelcacheByTuple(fc_reltup1);
		CacheInvalidateRelcacheByTuple(fc_reltup2);
	}

	/*
	 * 现在 pg_class 已用其相关信息更新以便于交换，更新关系的依赖项以指向其新的表 AM（如果已更改）。
	 */
	if (fc_relam1 != fc_relam2)
	{
		if (changeDependencyFor(RelationRelationId,
								fc_r1,
								AccessMethodRelationId,
								fc_relam1,
								fc_relam2) != 1)
			elog(ERROR, "failed to change access method dependency for relation \"%s.%s\"",
				 get_namespace_name(get_rel_namespace(fc_r1)),
				 get_rel_name(fc_r1));
		if (changeDependencyFor(RelationRelationId,
								fc_r2,
								AccessMethodRelationId,
								fc_relam2,
								fc_relam1) != 1)
			elog(ERROR, "failed to change access method dependency for relation \"%s.%s\"",
				 get_namespace_name(get_rel_namespace(fc_r2)),
				 get_rel_name(fc_r2));
	}

	/*
	 * 修改关系的后 alter 钩子。对 r2 的更改始终是内部的，但 r1 取决于调用上下文。
	 */
	InvokeObjectPostAlterHookArg(RelationRelationId, fc_r1, 0,
								 InvalidOid, fc_is_internal);
	InvokeObjectPostAlterHookArg(RelationRelationId, fc_r2, 0,
								 InvalidOid, true);

	/*
	 * 如果我们有与被交换关系关联的 toast 表，也处理它们。
	 */
	if (fc_relform1->reltoastrelid || fc_relform2->reltoastrelid)
	{
		if (fc_swap_toast_by_content)
		{
			if (fc_relform1->reltoastrelid && fc_relform2->reltoastrelid)
			{
				/* 递归交换 toast 表的内容 */
				fc_swap_relation_files(fc_relform1->reltoastrelid,
									fc_relform2->reltoastrelid,
									fc_target_is_pg_class,
									fc_swap_toast_by_content,
									fc_is_internal,
									fc_frozenXid,
									fc_cutoffMulti,
									fc_mapped_tables);
			}
			else
			{
				/* 调用者搞砸了 */
				elog(ERROR, "cannot swap toast files by content when there's only one");
			}
		}
		else
		{
			/*
			 * 我们交换了所有权链接，因此需要更改依赖数据以匹配。
			 *
			 * 注意：可能只有一个表具有 toast 表。
			 *
			 * 注意：目前，TOAST 表的唯一依赖是其拥有表的依赖。如果将来创建更多，我们需要使用比 deleteDependencyRecordsFor() 更具选择性的方式来仅删除我们想要的链接。
			 */
			ObjectAddress fc_baseobject,
						fc_toastobject;
			long		fc_count;

			/*
			 * 我们不允许这种情况发生在系统目录上，以避免
			 * 可能重建的目录是依赖项变化会改变的目录之一。
			 * 对目标目录进行任何数据更改已经为时已晚。
			 */
			if (IsSystemClass(fc_r1, fc_relform1))
				elog(ERROR, "cannot swap toast files by links for system catalogs");

			/* 删除旧依赖项 */
			if (fc_relform1->reltoastrelid)
			{
				fc_count = deleteDependencyRecordsFor(RelationRelationId,
												   fc_relform1->reltoastrelid,
												   false);
				if (fc_count != 1)
					elog(ERROR, "expected one dependency record for TOAST table, found %ld",
						 fc_count);
			}
			if (fc_relform2->reltoastrelid)
			{
				fc_count = deleteDependencyRecordsFor(RelationRelationId,
												   fc_relform2->reltoastrelid,
												   false);
				if (fc_count != 1)
					elog(ERROR, "expected one dependency record for TOAST table, found %ld",
						 fc_count);
			}

			/* 注册新依赖项 */
			fc_baseobject.classId = RelationRelationId;
			fc_baseobject.objectSubId = 0;
			fc_toastobject.classId = RelationRelationId;
			fc_toastobject.objectSubId = 0;

			if (fc_relform1->reltoastrelid)
			{
				fc_baseobject.objectId = fc_r1;
				fc_toastobject.objectId = fc_relform1->reltoastrelid;
				recordDependencyOn(&fc_toastobject, &fc_baseobject,
								   DEPENDENCY_INTERNAL);
			}

			if (fc_relform2->reltoastrelid)
			{
				fc_baseobject.objectId = fc_r2;
				fc_toastobject.objectId = fc_relform2->reltoastrelid;
				recordDependencyOn(&fc_toastobject, &fc_baseobject,
								   DEPENDENCY_INTERNAL);
			}
		}
	}

	/*
	 * 如果我们通过内容交换两个 toast 表，则对它们的
	 * 有效索引执行相同的操作。只有在关系具有索引时，
	 * 才可以安全地进行交换。
	 */
	if (fc_swap_toast_by_content &&
		fc_relform1->relkind == RELKIND_TOASTVALUE &&
		fc_relform2->relkind == RELKIND_TOASTVALUE)
	{
		Oid			fc_toastIndex1,
					fc_toastIndex2;

		/* 获取每个关系的有效索引 */
		fc_toastIndex1 = toast_get_valid_index(fc_r1,
											AccessExclusiveLock);
		fc_toastIndex2 = toast_get_valid_index(fc_r2,
											AccessExclusiveLock);

		fc_swap_relation_files(fc_toastIndex1,
							fc_toastIndex2,
							fc_target_is_pg_class,
							fc_swap_toast_by_content,
							fc_is_internal,
							InvalidTransactionId,
							InvalidMultiXactId,
							fc_mapped_tables);
	}

	/* 清理。 */
	heap_freetuple(fc_reltup1);
	heap_freetuple(fc_reltup2);

	table_close(fc_relRelation, RowExclusiveLock);

	/*
	 * 关闭两个 relcache 条目的 smgr 链接。我们需要这个权宜之计，
	 * 因为在即将到来的 CommandCounterIncrement 中，两个链接都会失效。
	 * 被清除的第二个关系将对另一个的 smgr 条目有一个悬挂的
	 * 引用。与其通过特定的顺序操作来避免这种情况，不如先关闭链接。
	 * （幸运的是，由于其中一个条目在我们的事务中是本地的，
	 * 因此以这种方式清除我们自己的 relcache 已足够；
	 * 当其他后端看到我们在非瞬态关系上的更新时，问题不会出现。）
	 *
	 * 注意：此步骤的位置与通过递归处理 toast rel 的决策有关。
	 * 当我们尝试重建 pg_class 本身时，pg_class 的 smgr 关闭
	 * 必须在此函数中的所有访问之后进行。
	 */
	RelationCloseSmgrByOid(fc_r1);
	RelationCloseSmgrByOid(fc_r2);
}

/*
 * 移除通过 make_new_heap 构建的瞬态表，完成
 * 清理（包括重建旧堆上的所有索引）。
 */
void finish_heap_swap(Oid fc_OIDOldHeap, Oid fc_OIDNewHeap,
				 bool fc_is_system_catalog,
				 bool fc_swap_toast_by_content,
				 bool fc_check_constraints,
				 bool fc_is_internal,
				 TransactionId fc_frozenXid,
				 MultiXactId fc_cutoffMulti,
				 char fc_newrelpersistence)
{
	ObjectAddress fc_object;
	Oid			fc_mapped_tables[4];
	int			fc_reindex_flags;
	ReindexParams fc_reindex_params = {0};
	int			fc_i;

	/* 报告我们现在正在交换关系文件 */
	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
								 PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES);

	/* 将可能的结果置零，从 swapped_relation_files 中 */
	memset(fc_mapped_tables, 0, sizeof(fc_mapped_tables));

	/*
	 * 交换堆关系的内容（包括任何 toast 表）。
	 * 还将旧堆的 relfrozenxid 设置为 frozenXid。
	 */
	fc_swap_relation_files(fc_OIDOldHeap, fc_OIDNewHeap,
						(fc_OIDOldHeap == RelationRelationId),
						fc_swap_toast_by_content, fc_is_internal,
						fc_frozenXid, fc_cutoffMulti, fc_mapped_tables);

	/*
	 * 如果是系统目录，在我们达到 CommandCounterIncrement 时
	 * 队列一个 sinval 消息以刷新目录上的所有 catcaches。
	 */
	if (fc_is_system_catalog)
		CacheInvalidateCatalog(fc_OIDOldHeap);

	/*
	 * 在关系上重建每个索引（但不包括 toast 表，因为
	 * 到此时它是全新的）。在 DROP 步骤之前执行此操作非常重要，
	 * 因为如果我们正在处理将在 DROP 期间使用的系统目录，
	 * 我们希望它的索引可用。无论如何，这种顺序都没有什么意义，
	 * 因为这一切都是事务性的，因此在提交之前没有机会回收磁盘空间。
	 * 我们不需要最后的 CommandCounterIncrement()，因为 reindex_relation 会处理。
	 *
	 * 注意：由于 index_build 是通过 reindex_relation 调用的，
	 * 它永远不会将 indcheckxmin 设置为 true。即使在某种意义上
	 * 我们是在构建新索引而不是重建现有索引，这也是可以的，
	 * 因为新堆根本不会包含任何 HOT 链，更不用说损坏的链，
	 * 所以不需要设置 indcheckxmin。
	 */
	fc_reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE;
	if (fc_check_constraints)
		fc_reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS;

	/*
	 * 确保索引与父关系具有相同的持久性。
	 */
	if (fc_newrelpersistence == RELPERSISTENCE_UNLOGGED)
		fc_reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
	else if (fc_newrelpersistence == RELPERSISTENCE_PERMANENT)
		fc_reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;

	/* 报告我们现在正在重新索引关系 */
	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
								 PROGRESS_CLUSTER_PHASE_REBUILD_INDEX);

	reindex_relation(fc_OIDOldHeap, fc_reindex_flags, &fc_reindex_params);

	/* 报告我们现在正在进行清理 */
	pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
								 PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP);

	/*
	 * 如果正在重建的关系是 pg_class，swap_relation_files()
	 * 无法更新 pg_class 自身的 pg_class 条目（请查看
	 * swap_relation_files() 中的注释），因此 relfrozenxid 并未
	 * 更新。这令人烦恼，因为进行 VACUUM FULL 的潜在原因是
	 * 即将或实际的反围绕停机。因此，现在我们可以使用其索引
	 * 访问新关系，更新 relfrozenxid。
	 * pg_class 没有 toast 关系，因此我们不需要更新相应的
	 * toast 关系。实际上，将所有 relfrozenxid 更新移动到
	 * 这里几乎没有意义，因为 swap_relation_files() 无论如何
	 * 需要对非映射关系写入 pg_class。
	 */
	if (fc_OIDOldHeap == RelationRelationId)
	{
		Relation	fc_relRelation;
		HeapTuple	fc_reltup;
		Form_pg_class fc_relform;

		fc_relRelation = table_open(RelationRelationId, RowExclusiveLock);

		fc_reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(fc_OIDOldHeap));
		if (!HeapTupleIsValid(fc_reltup))
			elog(ERROR, "cache lookup failed for relation %u", fc_OIDOldHeap);
		fc_relform = (Form_pg_class) GETSTRUCT(fc_reltup);

		fc_relform->relfrozenxid = fc_frozenXid;
		fc_relform->relminmxid = fc_cutoffMulti;

		CatalogTupleUpdate(fc_relRelation, &fc_reltup->t_self, fc_reltup);

		table_close(fc_relRelation, RowExclusiveLock);
	}

	/* 使用旧的 filenode 销毁新堆 */
	fc_object.classId = RelationRelationId;
	fc_object.objectId = fc_OIDNewHeap;
	fc_object.objectSubId = 0;

	/*
	 * 新关系仅对我们的事务是本地的，我们知道没有什么
	 * 依赖于它，因此 DROP_RESTRICT 应该没问题。
	 */
	performDeletion(&fc_object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);

	/* performDeletion 在最后执行 CommandCounterIncrement */

	/*
	 * 现在我们必须移除为瞬态表设置的任何关系映射条目，
	 * 以及它的 toast 表和 toast 索引（如果有的话）。如果
	 * 我们在提交之前未能做到这一点，relmapper 将会抱怨
	 * 添加了新的永久映射条目。
	 */
	for (fc_i = 0; OidIsValid(fc_mapped_tables[fc_i]); fc_i++)
		RelationMapRemoveMapping(fc_mapped_tables[fc_i]);

	/*
	 * 此时，除了如果我们通过链接进行了 toast 交换，toast
	 * 表的名称对应于瞬态表，其他一切都是正常的。该名称
	 * 对后端是无关紧要的，因为它是通过 OID 引用的，但
	 * 用户查看目录时可能会感到困惑。重命名它以防止这个问题。
	 *
	 * 注意：不需要对该关系加锁，因为我们已经持有独占锁。
	 */
	if (!fc_swap_toast_by_content)
	{
		Relation	fc_newrel;

		fc_newrel = table_open(fc_OIDOldHeap, NoLock);
		if (OidIsValid(fc_newrel->rd_rel->reltoastrelid))
		{
			Oid			fc_toastidx;
			char		fc_NewToastName[NAMEDATALEN];

			/* 获取要重命名的关联有效索引 */
			fc_toastidx = toast_get_valid_index(fc_newrel->rd_rel->reltoastrelid,
											 NoLock);

			/* 重命名 toast 表... */
			snprintf(fc_NewToastName, NAMEDATALEN, "pg_toast_%u",
					 fc_OIDOldHeap);
			RenameRelationInternal(fc_newrel->rd_rel->reltoastrelid,
								   fc_NewToastName, true, false);

			/* ... 以及它的有效索引。 */
			snprintf(fc_NewToastName, NAMEDATALEN, "pg_toast_%u_index",
					 fc_OIDOldHeap);

			RenameRelationInternal(fc_toastidx,
								   fc_NewToastName, true, true);

			/*
			 * 重置 toast 的 relrewrite。此处需要命令计数器
			 * 增加，因为我们即将更新在 RenameRelationInternal
			 * 过程中更新的元组。
			 */
			CommandCounterIncrement();
			ResetRelRewrite(fc_newrel->rd_rel->reltoastrelid);
		}
		relation_close(fc_newrel, NoLock);
	}

	/* 如果不是目录表，则清除所有缺失的属性设置 */
	if (!fc_is_system_catalog)
	{
		Relation	fc_newrel;

		fc_newrel = table_open(fc_OIDOldHeap, NoLock);
		RelationClearMissing(fc_newrel);
		relation_close(fc_newrel, NoLock);
	}
}


/*
 * 获取当前用户拥有且设置了 indisclustered 的表列表。
 * 以 List * 的形式返回 RelToCluster 列表（存储在指定的内存上下文中），
 * 每个条目给出 tableOid 和已聚类的索引 oid。
 */
static List * fc_get_tables_to_cluster(MemoryContext fc_cluster_context)
{
	Relation	fc_indRelation;
	TableScanDesc fc_scan;
	ScanKeyData fc_entry;
	HeapTuple	fc_indexTuple;
	Form_pg_index fc_index;
	MemoryContext fc_old_context;
	List	   *fc_rtcs = NIL;

	/*
	 * 获取所有设置了 indisclustered 的索引，并且属于
	 * 适当用户。
	 */
	fc_indRelation = table_open(IndexRelationId, AccessShareLock);
	ScanKeyInit(&fc_entry,
				Anum_pg_index_indisclustered,
				BTEqualStrategyNumber, F_BOOLEQ,
				BoolGetDatum(true));
	fc_scan = table_beginscan_catalog(fc_indRelation, 1, &fc_entry);
	while ((fc_indexTuple = heap_getnext(fc_scan, ForwardScanDirection)) != NULL)
	{
		RelToCluster *fc_rtc;

		fc_index = (Form_pg_index) GETSTRUCT(fc_indexTuple);

		if (!pg_class_ownercheck(fc_index->indrelid, GetUserId()))
			continue;

		/* 为结果列表使用永久内存上下文 */
		fc_old_context = MemoryContextSwitchTo(fc_cluster_context);

		fc_rtc = (RelToCluster *) palloc(sizeof(RelToCluster));
		fc_rtc->tableOid = fc_index->indrelid;
		fc_rtc->indexOid = fc_index->indexrelid;
		fc_rtcs = lappend(fc_rtcs, fc_rtc);

		MemoryContextSwitchTo(fc_old_context);
	}
	table_endscan(fc_scan);

	relation_close(fc_indRelation, AccessShareLock);

	return fc_rtcs;
}

/*
 * 给定一个分区表上的索引，返回所有子叶表/索引的 RelToCluster 列表。
 *
 * 类似于 expand_vacuum_rel，但这里调用者必须对包含索引的表
 * 持有 AccessExclusiveLock。
 */
static List * fc_get_tables_to_cluster_partitioned(MemoryContext fc_cluster_context, Oid fc_indexOid)
{
	List	   *fc_inhoids;
	ListCell   *fc_lc;
	List	   *fc_rtcs = NIL;
	MemoryContext fc_old_context;

	/* 在处理之前不要锁定子项 */
	fc_inhoids = find_all_inheritors(fc_indexOid, NoLock, NULL);

	foreach(fc_lc, fc_inhoids)
	{
		Oid			fc_indexrelid = lfirst_oid(fc_lc);
		Oid			fc_relid = IndexGetRelation(fc_indexrelid, false);
		RelToCluster *fc_rtc;

		/* 仅考虑叶索引 */
		if (get_rel_relkind(fc_indexrelid) != RELKIND_INDEX)
			continue;

		/* 默默跳过用户无权访问的分区。 */
		/* 对结果列表使用一个永久内存上下文 */
		if (!pg_class_ownercheck(fc_relid, GetUserId()) &&
			(!pg_database_ownercheck(MyDatabaseId, GetUserId()) ||
			 IsSharedRelation(fc_relid)))
			continue;

		/* 为结果列表使用永久内存上下文 */
		fc_old_context = MemoryContextSwitchTo(fc_cluster_context);

		fc_rtc = (RelToCluster *) palloc(sizeof(RelToCluster));
		fc_rtc->tableOid = fc_relid;
		fc_rtc->indexOid = fc_indexrelid;
		fc_rtcs = lappend(fc_rtcs, fc_rtc);

		MemoryContextSwitchTo(fc_old_context);
	}

	return fc_rtcs;
}
